{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "SJ5mOR93_b7L"
      },
      "source": [
        "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mongodb-developer/GenAI-Showcase/blob/main/notebooks/rag/self_querying_mongodb_unstructured_langgraph.ipynb)\n",
        "\n",
        "[![View Article](https://img.shields.io/badge/View%20Article-blue)](https://www.mongodb.com/developer/products/atlas/advanced-rag-self-querying-retrieval/?utm_campaign=devrel&utm_source=cross-post&utm_medium=organic_social&utm_content=https%3A%2F%2Fgithub.com%2Fmongodb-developer%2FGenAI-Showcase&utm_term=apoorva.joshi)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "6swbsMJaCw7v"
      },
      "source": [
        "# Building an Advanced RAG System with Self-Querying Retrieval\n",
        "\n",
        "This notebook shows how to incorporate self-querying retrieval into a RAG application using Unstructured, MongoDB and LangGraph."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "sCD6whdjRIdw"
      },
      "source": [
        "## Step 1: Install required libraries\n",
        "\n",
        "- **langgraph**: Python package to build stateful, multi-actor applications with LLMs\n",
        "<p>\n",
        "- **openai**: Python package to interact with OpenAI APIs\n",
        "<p>\n",
        "- **pymongo**: Python package to interact with MongoDB databases and collections\n",
        "<p>\n",
        "- **sentence-transformers**: Python package for open-source language models\n",
        "<p>\n",
        "- **unstructured-ingest**: Python package for data processing using Unstructured"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "MjRkV7PX9Uik"
      },
      "outputs": [],
      "source": [
        "!pip install -qU langgraph openai pymongo sentence-transformers \"unstructured-ingest[pdf, s3, mongodb, embed-huggingface]\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "qc8q6Hx6Rwm-"
      },
      "outputs": [],
      "source": [
        "import warnings\n",
        "\n",
        "warnings.filterwarnings(\"ignore\", category=UserWarning)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "yh1ZHga1HM8p"
      },
      "source": [
        "## Step 2: Setup prerequisites\n",
        "\n",
        "- **Set the Unstructured API key and URL**: Steps to obtain the API key and URL are [here](https://unstructured.io/api-key-hosted)\n",
        "\n",
        "- **Set the AWS access keys**: Steps to obtain the AWS access keys are [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html)\n",
        "\n",
        "- **Set the MongoDB connection string**: Follow the steps [here](https://www.mongodb.com/docs/manual/reference/connection-string/) to get the connection string from the Atlas UI.\n",
        "\n",
        "- **Set the OpenAI API key**: Steps to obtain an API key as [here](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "o4Tr1N2URwEn"
      },
      "outputs": [],
      "source": [
        "import os\n",
        "\n",
        "from openai import OpenAI\n",
        "from pymongo import MongoClient"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "ExecuteTime": {
          "end_time": "2024-08-22T20:25:13.754843Z",
          "start_time": "2024-08-22T20:25:13.752362Z"
        },
        "id": "XgVmhd7188oC"
      },
      "outputs": [],
      "source": [
        "# Your Unstructured API key and URL\n",
        "UNSTRUCTURED_API_KEY = \"\"\n",
        "UNSTRUCTURED_URL = \"\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "-CUA7sXSU0Jj"
      },
      "outputs": [],
      "source": [
        "# Your AWS authentication credentials\n",
        "AWS_KEY = \"\"\n",
        "AWS_SECRET = \"\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "YaZW_HLlU2Rp"
      },
      "outputs": [],
      "source": [
        "# S3 URI for the Access Point to the bucket with PDF files\n",
        "AWS_S3_NAME = \"\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "WzidEkTXU5zp"
      },
      "outputs": [],
      "source": [
        "# Your MongoDB connection string (uri), and collection/database names\n",
        "MONGODB_URI = \"\"\n",
        "MONGODB_DB_NAME = \"\"\n",
        "MONGODB_COLLECTION = \"\"\n",
        "# Instantiate the MongoDB client\n",
        "mongodb_client = MongoClient(\n",
        "    MONGODB_URI, appname=\"devrel.showcase.selfquery_mongodb_unstructured\"\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "kZ5DqBxWVE2l"
      },
      "outputs": [],
      "source": [
        "# Your OpenAI API key\n",
        "os.environ[\"OPENAI_API_KEY\"] = \"\"\n",
        "# Instantiate the OpenAI client\n",
        "openai_client = OpenAI()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "metadata": {
        "id": "K1iP8H9ezash"
      },
      "outputs": [],
      "source": [
        "# Embedding model to use\n",
        "EMBEDDING_MODEL_NAME = \"BAAI/bge-base-en-v1.5\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "metadata": {
        "id": "dRx6CT1Ezasi"
      },
      "outputs": [],
      "source": [
        "# Completion model to use\n",
        "COMPLETION_MODEL_NAME = \"gpt-4o-2024-08-06\""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4XAZrBVM9eqV"
      },
      "source": [
        "## Step 3: Partition, chunk and embed PDF files\n",
        "\n",
        "Let's set up the PDF preprocessing pipeline with Unstructured. The pipeline will:\n",
        "1. Ingest data from an S3 bucket/local directory\n",
        "2. Partition documents: extract text and metadata, split the documents into document elements, such as titles, paragraphs (narrative text), tables, images, lists, etc. Learn more about document elements in [Unstructured documentation])https://docs.unstructured.io/api-reference/api-services/document-elements).\n",
        "3. Chunk the documents.\n",
        "4. Embed the documents with the [`BAAI/bge-base-en-v1.5`](https://huggingface.co/BAAI/bge-base-en-v1.5) embedding model the Hugging Face Hub.\n",
        "5. Save the results locally."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "gn5FHZI7ZJ7A"
      },
      "outputs": [],
      "source": [
        "from unstructured_ingest.v2.interfaces import ProcessorConfig\n",
        "from unstructured_ingest.v2.pipeline.pipeline import Pipeline\n",
        "from unstructured_ingest.v2.processes.chunker import ChunkerConfig\n",
        "from unstructured_ingest.v2.processes.connectors.fsspec.s3 import (\n",
        "    S3AccessConfig,\n",
        "    S3ConnectionConfig,\n",
        "    S3DownloaderConfig,\n",
        "    S3IndexerConfig,\n",
        ")\n",
        "\n",
        "# For pipeline using a local source\n",
        "# from unstructured_ingest.v2.processes.connectors.local import (\n",
        "#     LocalIndexerConfig,\n",
        "#     LocalDownloaderConfig,\n",
        "#     LocalConnectionConfig,\n",
        "# )\n",
        "from unstructured_ingest.v2.processes.connectors.local import LocalUploaderConfig\n",
        "from unstructured_ingest.v2.processes.embedder import EmbedderConfig\n",
        "from unstructured_ingest.v2.processes.partitioner import PartitionerConfig"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "ExecuteTime": {
          "end_time": "2024-08-22T20:25:18.555381Z",
          "start_time": "2024-08-22T20:25:18.553677Z"
        },
        "id": "Z5v_JWSBwbr2"
      },
      "outputs": [],
      "source": [
        "WORK_DIR = \"/content/temp\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "ExecuteTime": {
          "end_time": "2024-08-22T21:36:12.887187Z",
          "start_time": "2024-08-22T21:22:53.915454Z"
        },
        "id": "0h020TAN9nMB"
      },
      "outputs": [],
      "source": [
        "Pipeline.from_configs(\n",
        "    context=ProcessorConfig(\n",
        "        verbose=True, tqdm=True, num_processes=5, work_dir=WORK_DIR\n",
        "    ),\n",
        "    indexer_config=S3IndexerConfig(remote_url=AWS_S3_NAME),\n",
        "    downloader_config=S3DownloaderConfig(),\n",
        "    source_connection_config=S3ConnectionConfig(\n",
        "        access_config=S3AccessConfig(key=AWS_KEY, secret=AWS_SECRET)\n",
        "    ),\n",
        "    # For pipeline using a local source\n",
        "    # indexer_config=LocalIndexerConfig(input_path=\"your_local_directory\"),\n",
        "    # downloader_config=LocalDownloaderConfig(),\n",
        "    # source_connection_config=LocalConnectionConfig(),\n",
        "    partitioner_config=PartitionerConfig(\n",
        "        partition_by_api=True,\n",
        "        api_key=UNSTRUCTURED_API_KEY,\n",
        "        partition_endpoint=UNSTRUCTURED_URL,\n",
        "        strategy=\"hi_res\",\n",
        "        additional_partition_args={\n",
        "            \"split_pdf_page\": True,\n",
        "            \"split_pdf_allow_failed\": True,\n",
        "            \"split_pdf_concurrency_level\": 15,\n",
        "        },\n",
        "    ),\n",
        "    chunker_config=ChunkerConfig(\n",
        "        chunking_strategy=\"by_title\",\n",
        "        chunk_max_characters=1500,\n",
        "        chunk_overlap=150,\n",
        "    ),\n",
        "    embedder_config=EmbedderConfig(\n",
        "        embedding_provider=\"langchain-huggingface\",\n",
        "        embedding_model_name=EMBEDDING_MODEL_NAME,\n",
        "    ),\n",
        "    uploader_config=LocalUploaderConfig(output_dir=\"/content/ingest-outputs\"),\n",
        ").run()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ZAFAUsSB-_6p"
      },
      "source": [
        "## Step 4: Add custom metadata to the processed documents\n",
        "\n",
        "For each document, we want to add the company name and fiscal year as custom metadata, to enable smart pre-filtering for more precise document retrieval.\n",
        "\n",
        "Luckily the Form-10K documents have a more or less standard page with this information, so we can use regex to extract this information."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "6WxHUHkZaJ4a"
      },
      "outputs": [],
      "source": [
        "import json\n",
        "import re"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "ExecuteTime": {
          "end_time": "2024-08-22T21:42:16.571027Z",
          "start_time": "2024-08-22T21:42:16.565939Z"
        },
        "id": "wPr1vj_BNt2j"
      },
      "outputs": [],
      "source": [
        "def get_fiscal_year(elements: dict) -> int:\n",
        "    \"\"\"\n",
        "    Extract fiscal year from document elements.\n",
        "\n",
        "    Args:\n",
        "        elements (dict): Document elements\n",
        "\n",
        "    Returns:\n",
        "        int: Year\n",
        "    \"\"\"\n",
        "    # Regular expression pattern to find the element containing the fiscal year\n",
        "    pattern = r\"for the (fiscal\\s+)?year ended.*?(\\d{4})\"\n",
        "    year = 0\n",
        "    for i in range(len(elements)):\n",
        "        match = re.search(pattern, elements[i][\"text\"], re.IGNORECASE)\n",
        "        if match:\n",
        "            year = match.group(0)[-4:]\n",
        "            try:\n",
        "                year = int(year)\n",
        "            except Exception:\n",
        "                year = 0\n",
        "    return year"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "def get_company_name(elements: dict) -> str:\n",
        "    \"\"\"\n",
        "    Extract company name from document elements.\n",
        "\n",
        "    Args:\n",
        "        elements (dict): Document elements\n",
        "\n",
        "    Returns:\n",
        "        str: Company name\n",
        "    \"\"\"\n",
        "    name = \"\"\n",
        "    # In most cases the name of the company is right before/above the following line\n",
        "    substring = \"(Exact name of registrant as specified\"\n",
        "    for i in range(len(elements)):\n",
        "        if substring.lower() in elements[i][\"text\"].lower():\n",
        "            pattern = (\n",
        "                r\"([A-Z][A-Za-z\\s&.,]+?)\\s*\\(Exact name of registrant as specified\"\n",
        "            )\n",
        "            match = re.search(pattern, elements[i][\"text\"], re.IGNORECASE)\n",
        "            if match:\n",
        "                name = match.group(1).strip()\n",
        "                name = name.split(\"\\n\\n\")[-1]\n",
        "\n",
        "    if name == \"\":\n",
        "        for i in range(len(elements)):\n",
        "            # In some cases, the name of the company is right after/below the following line\n",
        "            match = re.search(\n",
        "                r\"Exact name of registrant as specified in its charter:\\n\\n(.*?)\\n\\n\",\n",
        "                elements[i][\"text\"],\n",
        "            )\n",
        "            if match:\n",
        "                name = match.group(1)\n",
        "            else:\n",
        "                # In other cases, the name follows the \"Commission File Number [Number]\" line\n",
        "                match = re.search(\n",
        "                    r\"Commission File Number.*\\n\\n(.*?)\\n\\n\", elements[i][\"text\"]\n",
        "                )\n",
        "                if match:\n",
        "                    name = match.group(1)\n",
        "    return name"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "INX733naO_kZ"
      },
      "source": [
        "We'll walk through the directory with the embedding results, and for each document find the company name and year, and add it as custom metadata to all elements of the document."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "directory = f\"{WORK_DIR}/embed\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "ExecuteTime": {
          "end_time": "2024-08-22T21:42:17.201191Z",
          "start_time": "2024-08-22T21:42:17.197763Z"
        },
        "id": "m8guacI4wY7J"
      },
      "outputs": [],
      "source": [
        "for filename in os.listdir(directory):\n",
        "    if filename.endswith(\".json\"):\n",
        "        file_path = os.path.join(directory, filename)\n",
        "        print(f\"Processing file {filename}\")\n",
        "        try:\n",
        "            with open(file_path) as file:\n",
        "                data = json.load(file)\n",
        "\n",
        "            company_name = get_company_name(data)\n",
        "            fiscal_year = get_fiscal_year(data)\n",
        "\n",
        "            # Add custom metadata fields to each entry\n",
        "            for entry in data:\n",
        "                entry[\"metadata\"][\"custom_metadata\"] = {}\n",
        "                entry[\"metadata\"][\"custom_metadata\"][\"company\"] = company_name\n",
        "                entry[\"metadata\"][\"custom_metadata\"][\"year\"] = fiscal_year\n",
        "\n",
        "            with open(file_path, \"w\") as file:\n",
        "                json.dump(data, file, indent=2)\n",
        "\n",
        "            print(f\"Successfully updated {file_path} with custom metadata fields.\")\n",
        "        except json.JSONDecodeError as e:\n",
        "            print(f\"Error parsing JSON in {file_path}: {e}\")\n",
        "        except OSError as e:\n",
        "            print(f\"Error reading from or writing to {file_path}: {e}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Y1xDPo0VyHYY"
      },
      "source": [
        "## Step 5: Write the processed documents to MongoDB\n",
        "\n",
        "To write the final processed documents to MongoDB, we will need to rerun the same pipeline, except we'll now change the destination from local to MongoDB.\n",
        "The pipeline will not repeat partitioning, chunking and embedding steps, since there are results for them already cached in the `WORK_DIR`. It will pick up the customized embedding results and load them into a MongoDB collection.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "3ybGRAFlbFOr"
      },
      "outputs": [],
      "source": [
        "from unstructured_ingest.v2.processes.connectors.mongodb import (\n",
        "    MongoDBAccessConfig,\n",
        "    MongoDBConnectionConfig,\n",
        "    MongoDBUploaderConfig,\n",
        "    MongoDBUploadStagerConfig,\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "ExecuteTime": {
          "end_time": "2024-08-22T21:49:34.578823Z",
          "start_time": "2024-08-22T21:45:03.739789Z"
        },
        "id": "k7dhENwv_HHH"
      },
      "outputs": [],
      "source": [
        "Pipeline.from_configs(\n",
        "    context=ProcessorConfig(\n",
        "        verbose=True, tqdm=True, num_processes=5, work_dir=WORK_DIR\n",
        "    ),\n",
        "    indexer_config=S3IndexerConfig(remote_url=AWS_S3_NAME),\n",
        "    downloader_config=S3DownloaderConfig(),\n",
        "    source_connection_config=S3ConnectionConfig(\n",
        "        access_config=S3AccessConfig(key=AWS_KEY, secret=AWS_SECRET)\n",
        "    ),\n",
        "    partitioner_config=PartitionerConfig(\n",
        "        partition_by_api=True,\n",
        "        api_key=UNSTRUCTURED_API_KEY,\n",
        "        partition_endpoint=UNSTRUCTURED_URL,\n",
        "        strategy=\"hi_res\",\n",
        "        additional_partition_args={\n",
        "            \"split_pdf_page\": True,\n",
        "            \"split_pdf_allow_failed\": True,\n",
        "            \"split_pdf_concurrency_level\": 15,\n",
        "        },\n",
        "    ),\n",
        "    chunker_config=ChunkerConfig(\n",
        "        chunking_strategy=\"by_title\",\n",
        "        chunk_max_characters=1500,\n",
        "        chunk_overlap=150,\n",
        "    ),\n",
        "    embedder_config=EmbedderConfig(\n",
        "        embedding_provider=\"langchain-huggingface\",\n",
        "        embedding_model_name=EMBEDDING_MODEL_NAME,\n",
        "    ),\n",
        "    destination_connection_config=MongoDBConnectionConfig(\n",
        "        access_config=MongoDBAccessConfig(uri=MONGODB_URI),\n",
        "        collection=MONGODB_COLLECTION,\n",
        "        database=MONGODB_DB_NAME,\n",
        "    ),\n",
        "    stager_config=MongoDBUploadStagerConfig(),\n",
        "    uploader_config=MongoDBUploaderConfig(batch_size=100),\n",
        ").run()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "mjzSYCcoelXD"
      },
      "source": [
        "Next, we are going to use LangGraph to build our investment assistant. With LangGraph, we can build LLM systems as graphs with a shared state, conditional edges, and cyclic loops between nodes."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "vwbGD1Zvoo-v"
      },
      "source": [
        "## Step 6: Define graph state\n",
        "\n",
        "Let's first define the state of our graph. The state is a mutable object that tracks different attributes as we pass through the nodes in the graph. We can include custom attributes within the state that represent parameters we want to track."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "WjkvhhGvpXyX"
      },
      "outputs": [],
      "source": [
        "from typing import Annotated, Dict, List\n",
        "\n",
        "from langgraph.graph.message import add_messages\n",
        "from typing_extensions import TypedDict"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "OMY4obokpZ8-"
      },
      "outputs": [],
      "source": [
        "class GraphState(TypedDict):\n",
        "    \"\"\"\n",
        "    Represents the state of the graph.\n",
        "\n",
        "    Attributes:\n",
        "        question: User query\n",
        "        metadata: Extracted metadata\n",
        "        filter: Filter definition\n",
        "        documents: List of retrieved documents from vector search\n",
        "        memory: Conversational history\n",
        "    \"\"\"\n",
        "\n",
        "    question: str\n",
        "    metadata: Dict\n",
        "    filter: Dict\n",
        "    context: List[str]\n",
        "    memory: Annotated[list, add_messages]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "1_Y0dn4Zpbrx"
      },
      "source": [
        "## Step 7: Define graph nodes\n",
        "\n",
        "Next, let's add the graph nodes. Nodes in LangGraph are functions or tools that your system has access to in order to complete the task. Each node updates one or more attributes in the graph state with its return value after it executes. Our assistant has four nodes:\n",
        "1. **Metadata Extractor**: Extract metadata from a natural language query\n",
        "2. **Filter Generator**: Generate a MongoDB Query API filter definition\n",
        "3. **MongoDB Atlas Vector Search**: Retrieve documents from MongoDB using semantic search\n",
        "4. **Answer Generator**: Generate an answer to the user question\n",
        "\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "i2cdww8ys1tl"
      },
      "source": [
        "### Metadata Extractor"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "wnMECC9PszGl"
      },
      "outputs": [],
      "source": [
        "from datetime import datetime\n",
        "\n",
        "from pydantic import BaseModel, Field"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "_ik88ZXms5Pj"
      },
      "outputs": [],
      "source": [
        "companies = [\n",
        "    \"AT&T INC.\",\n",
        "    \"American International Group, Inc.\",\n",
        "    \"Apple Inc.\",\n",
        "    \"BERKSHIRE HATHAWAY INC.\",\n",
        "    \"Bank of America Corporation\",\n",
        "    \"CENCORA, INC.\",\n",
        "    \"CVS HEALTH CORPORATION\",\n",
        "    \"Cardinal Health, Inc.\",\n",
        "    \"Chevron Corporation\",\n",
        "    \"Citigroup Inc.\",\n",
        "    \"Costco Wholesale Corporation\",\n",
        "    \"Exxon Mobil Corporation\",\n",
        "    \"Ford Motor Company\",\n",
        "    \"GENERAL ELECTRIC COMPANY\",\n",
        "    \"GENERAL MOTORS COMPANY\",\n",
        "    \"HP Inc.\",\n",
        "    \"INTERNATIONAL BUSINESS MACHINES CORPORATION\",\n",
        "    \"JPMorgan Chase & Co.\",\n",
        "    \"MICROSOFT CORPORATION\",\n",
        "    \"MIDLAND COMPANY\",\n",
        "    \"McKESSON CORPORATION\",\n",
        "    \"THE BOEING COMPANY\",\n",
        "    \"THE HOME DEPOT, INC.\",\n",
        "    \"THE KROGER CO.\",\n",
        "    \"The Goldman Sachs Group, Inc.\",\n",
        "    \"UnitedHealth Group Incorporated\",\n",
        "    \"VALERO ENERGY CORPORATION\",\n",
        "    \"Verizon Communications Inc.\",\n",
        "    \"WALMART INC.\",\n",
        "    \"WELLS FARGO & COMPANY\",\n",
        "]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "cK3Pd9HXs7P2"
      },
      "outputs": [],
      "source": [
        "class Metadata(BaseModel):\n",
        "    \"\"\"Metadata to use for pre-filtering.\"\"\"\n",
        "\n",
        "    company: List[str] = Field(description=\"List of company names\")\n",
        "    year: List[str] = Field(description=\"List containing start year and end year\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "jvPjpPkZtAWr"
      },
      "outputs": [],
      "source": [
        "def extract_metadata(state: Dict) -> Dict:\n",
        "    \"\"\"\n",
        "    Extract metadata from natural language query.\n",
        "\n",
        "    Args:\n",
        "        state (Dict): The current graph state\n",
        "\n",
        "    Returns:\n",
        "        Dict: New key added to state i.e. metadata containing the metadata extracted from the user query.\n",
        "    \"\"\"\n",
        "    print(\"---EXTRACTING METADATA---\")\n",
        "    question = state[\"question\"]\n",
        "    system = f\"\"\"Extract the specified metadata from the user question:\n",
        "    - company: List of company names, eg: Google, Adobe etc. Match the names to companies on this list: {companies}\n",
        "    - year: List of [start year, end year]. Guidelines for extracting dates:\n",
        "        - If a single date is found, only include that.\n",
        "        - For phrases like 'in the past X years/last year', extract the start year by subtracting X from the current year. The current year is {datetime.now().year}.\n",
        "        - If more than two dates are found, only include the smallest and the largest year.\"\"\"\n",
        "    completion = openai_client.beta.chat.completions.parse(\n",
        "        model=COMPLETION_MODEL_NAME,\n",
        "        messages=[\n",
        "            {\"role\": \"system\", \"content\": system},\n",
        "            {\"role\": \"user\", \"content\": question},\n",
        "        ],\n",
        "        response_format=Metadata,\n",
        "    )\n",
        "    result = completion.choices[0].message.parsed\n",
        "    # If no metadata is extracted return an empty dictionary\n",
        "    if len(result.company) == 0 and len(result.year) == 0:\n",
        "        return {\"metadata\": {}}\n",
        "    metadata = {\n",
        "        \"metadata.custom_metadata.company\": result.company,\n",
        "        \"metadata.custom_metadata.year\": result.year,\n",
        "    }\n",
        "    return {\"metadata\": metadata}"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "EoQH0sSDtB7_"
      },
      "source": [
        "### Filter Generator"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "cKgLDhGdtBFp"
      },
      "outputs": [],
      "source": [
        "def generate_filter(state: Dict) -> Dict:\n",
        "    \"\"\"\n",
        "    Generate MongoDB Query API filter definition.\n",
        "\n",
        "    Args:\n",
        "        state (Dict): The current graph state\n",
        "\n",
        "    Returns:\n",
        "        Dict: New key added to state i.e. filter.\n",
        "    \"\"\"\n",
        "    print(\"---GENERATING FILTER DEFINITION---\")\n",
        "    metadata = state[\"metadata\"]\n",
        "    system = \"\"\"Generate a MongoDB filter definition from the provided fields. Follow the guidelines below:\n",
        "    - Respond in JSON with the filter assigned to a `filter` key.\n",
        "    - The field `metadata.custom_metadata.company` is a list of companies.\n",
        "    - The field `metadata.custom_metadata.year` is a list of one or more years.\n",
        "    - If any of the provided fields are empty lists, DO NOT include them in the filter.\n",
        "    - If both the metadata fields are empty lists, return an empty dictionary {{}}.\n",
        "    - The filter should only contain the fields `metadata.custom_metadata.company` and `metadata.custom_metadata.year`\n",
        "    - The filter can only contain the following MongoDB Query API match expressions:\n",
        "        - $gt: Greater than\n",
        "        - $lt: Lesser than\n",
        "        - $gte: Greater than or equal to\n",
        "        - $lte: Less than or equal to\n",
        "        - $eq: Equal to\n",
        "        - $ne: Not equal to\n",
        "        - $in: Specified field value equals any value in the specified array\n",
        "        - $nin: Specified field value is not present in the specified array\n",
        "        - $nor: Logical NOR operation\n",
        "        - $and: Logical AND operation\n",
        "        - $or: Logical OR operation\n",
        "    - If the `metadata.custom_metadata.year` field has multiple dates, create a date range filter using expressions such as $gt, $lt, $lte and $gte\n",
        "    - If the `metadata.custom_metadata.company` field contains a single company, use the $eq expression\n",
        "    - If the `metadata.custom_metadata.company` field contains multiple companies, use the $in expression\n",
        "    - To combine date range and company filters, use the $and operator\n",
        "    \"\"\"\n",
        "    completion = openai_client.chat.completions.create(\n",
        "        model=COMPLETION_MODEL_NAME,\n",
        "        temperature=0,\n",
        "        messages=[\n",
        "            {\"role\": \"system\", \"content\": system},\n",
        "            {\"role\": \"user\", \"content\": f\"Fields: {metadata}\"},\n",
        "        ],\n",
        "        response_format={\"type\": \"json_object\"},\n",
        "    )\n",
        "    result = json.loads(completion.choices[0].message.content)\n",
        "    return {\"filter\": result.get(\"filter\", {})}"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "U1nFUwqbtGtS"
      },
      "source": [
        "### MongoDB Atlas Vector Search"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ECTcQQo_t7Zl"
      },
      "outputs": [],
      "source": [
        "from sentence_transformers import SentenceTransformer"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "A36BU562tFwD"
      },
      "outputs": [],
      "source": [
        "embedding_model = SentenceTransformer(EMBEDDING_MODEL_NAME)\n",
        "collection = mongodb_client[MONGODB_DB_NAME][MONGODB_COLLECTION]\n",
        "VECTOR_SEARCH_INDEX_NAME = \"vector_index\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Create a vector search index\n",
        "model = {\n",
        "    \"name\": VECTOR_SEARCH_INDEX_NAME,\n",
        "    \"type\": \"vectorSearch\",\n",
        "    \"definition\": {\n",
        "        \"fields\": [\n",
        "            {\n",
        "                \"type\": \"vector\",\n",
        "                \"path\": \"embeddings\",\n",
        "                \"numDimensions\": 768,\n",
        "                \"similarity\": \"cosine\",\n",
        "            },\n",
        "            {\"type\": \"filter\", \"path\": \"metadata.custom_metadata.company\"},\n",
        "            {\"type\": \"filter\", \"path\": \"metadata.custom_metadata.year\"},\n",
        "        ]\n",
        "    },\n",
        "}\n",
        "collection.create_search_index(model=model)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "N7JWGig4uUUX"
      },
      "outputs": [],
      "source": [
        "def vector_search(state: Dict) -> Dict:\n",
        "    \"\"\"\n",
        "    Get relevant information using MongoDB Atlas Vector Search\n",
        "\n",
        "    Args:\n",
        "        state (Dict): The current graph state\n",
        "\n",
        "    Returns:\n",
        "        Dict: New key added to state i.e. documents.\n",
        "    \"\"\"\n",
        "    print(\"---PERFORMING VECTOR SEARCH---\")\n",
        "    question = state[\"question\"]\n",
        "    filter = state[\"filter\"]\n",
        "    # We always want a valid filter object\n",
        "    if not filter:\n",
        "        filter = {}\n",
        "    query_embedding = embedding_model.encode(question).tolist()\n",
        "    pipeline = [\n",
        "        {\n",
        "            \"$vectorSearch\": {\n",
        "                \"index\": VECTOR_SEARCH_INDEX_NAME,\n",
        "                \"path\": \"embeddings\",\n",
        "                \"queryVector\": query_embedding,\n",
        "                \"numCandidates\": 150,\n",
        "                \"limit\": 5,\n",
        "                \"filter\": filter,\n",
        "            }\n",
        "        },\n",
        "        {\n",
        "            \"$project\": {\n",
        "                \"_id\": 0,\n",
        "                \"text\": 1,\n",
        "                \"score\": {\"$meta\": \"vectorSearchScore\"},\n",
        "            }\n",
        "        },\n",
        "    ]\n",
        "    # Execute the aggregation pipeline\n",
        "    results = collection.aggregate(pipeline)\n",
        "    # Drop documents with cosine similarity score < 0.8\n",
        "    relevant_results = [doc[\"text\"] for doc in results if doc[\"score\"] >= 0.8]\n",
        "    context = \"\\n\\n\".join([doc for doc in relevant_results])\n",
        "    return {\"context\": context}"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "xxOxaEpyuYhr"
      },
      "source": [
        "### Answer Generator"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "YVLcD3Ajzaso"
      },
      "outputs": [],
      "source": [
        "from langchain_core.messages import AIMessage, HumanMessage"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "032eyrshuoHD"
      },
      "outputs": [],
      "source": [
        "def generate_answer(state: Dict) -> Dict:\n",
        "    \"\"\"\n",
        "    Generate the final answer to the user query\n",
        "\n",
        "    Args:\n",
        "        state (Dict): The current graph state\n",
        "\n",
        "    Returns:\n",
        "        Dict: New key added to state i.e. generation.\n",
        "    \"\"\"\n",
        "    print(\"---GENERATING THE ANSWER---\")\n",
        "    question = state[\"question\"]\n",
        "    context = state[\"context\"]\n",
        "    memory = state[\"memory\"]\n",
        "    system = \"Answer the question based only on the following context. If the context is empty or if it doesn't provide enough information to answer the question, say I DON'T KNOW\"\n",
        "    completion = openai_client.chat.completions.create(\n",
        "        model=COMPLETION_MODEL_NAME,\n",
        "        temperature=0,\n",
        "        messages=[\n",
        "            {\"role\": \"system\", \"content\": system},\n",
        "            {\n",
        "                \"role\": \"user\",\n",
        "                \"content\": f\"Context:\\n{context}\\n\\n{memory}\\n\\nQuestion:{question}\",\n",
        "            },\n",
        "        ],\n",
        "    )\n",
        "    answer = completion.choices[0].message.content\n",
        "    return {\"memory\": [HumanMessage(content=context), AIMessage(content=answer)]}"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "z_W0_wjmupb4"
      },
      "source": [
        "## Step 8: Define conditional edges\n",
        "\n",
        "Conditional edges in LangGraph decide which node in the graph to visit next. Here, we have a single conditional edge to skip filter generation and go directly to the vector search step if no metadata was extracted from the user query."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "UyG52PLivGfc"
      },
      "outputs": [],
      "source": [
        "def check_metadata_extracted(state: Dict) -> str:\n",
        "    \"\"\"\n",
        "    Check if any metadata is extracted.\n",
        "\n",
        "    Args:\n",
        "        state (Dict): The current graph state\n",
        "\n",
        "    Returns:\n",
        "        str: Binary decision for next node to call\n",
        "    \"\"\"\n",
        "\n",
        "    print(\"---CHECK FOR METADATA---\")\n",
        "    metadata = state[\"metadata\"]\n",
        "    # If no metadata is extracted, skip the generate filter step\n",
        "    if not metadata:\n",
        "        print(\"---DECISION: SKIP TO VECTOR SEARCH---\")\n",
        "        return \"vector_search\"\n",
        "    # If metadata is extracted, generate filter definition\n",
        "    print(\"---DECISION: GENERATE FILTER---\")\n",
        "    return \"generate_filter\""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "u4_Kx9X_vJ1m"
      },
      "source": [
        "## Step 9: Build the graph/flow\n",
        "\n",
        "This is where we actually define the flow of the graph by connecting nodes to edges."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "LeEusekIvIXZ"
      },
      "outputs": [],
      "source": [
        "from IPython.display import Image, display\n",
        "from langgraph.checkpoint.memory import MemorySaver\n",
        "from langgraph.graph import END, START, StateGraph"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "2fuGB9c7vZWp"
      },
      "outputs": [],
      "source": [
        "workflow = StateGraph(GraphState)\n",
        "# Adding memory to the graph\n",
        "memory = MemorySaver()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "6FG5Kb7Zva2r"
      },
      "outputs": [],
      "source": [
        "# Add nodes\n",
        "workflow.add_node(\"extract_metadata\", extract_metadata)\n",
        "workflow.add_node(\"generate_filter\", generate_filter)\n",
        "workflow.add_node(\"vector_search\", vector_search)\n",
        "workflow.add_node(\"generate_answer\", generate_answer)\n",
        "\n",
        "# Add edges\n",
        "workflow.add_edge(START, \"extract_metadata\")\n",
        "workflow.add_conditional_edges(\n",
        "    \"extract_metadata\",\n",
        "    check_metadata_extracted,\n",
        "    {\n",
        "        \"vector_search\": \"vector_search\",\n",
        "        \"generate_filter\": \"generate_filter\",\n",
        "    },\n",
        ")\n",
        "workflow.add_edge(\"generate_filter\", \"vector_search\")\n",
        "workflow.add_edge(\"vector_search\", \"generate_answer\")\n",
        "workflow.add_edge(\"generate_answer\", END)\n",
        "\n",
        "# Compile the graph\n",
        "app = workflow.compile(checkpointer=memory)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 44,
      "metadata": {
        "id": "Q1DWV6davdvC",
        "outputId": "7f3517fd-571a-4cff-b895-917e6f861ab9"
      },
      "outputs": [
        {
          "data": {
            "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAITANkDASIAAhEBAxEB/8QAHQABAAMAAwEBAQAAAAAAAAAAAAUGBwMECAECCf/EAFsQAAEDAwEDBgcIDQcKBQUAAAEAAgMEBQYRBxIhExUxQVaUCBQWFyJR0zI2VFVhdNHSIzRCcXN1gZKTlbK01CZSYpGhscIJGCQlMzVFs/DxQ0RTY8FHV3Kio//EABoBAQEAAwEBAAAAAAAAAAAAAAABAgQFAwb/xAA1EQEAAQICBgcHBAMBAAAAAAAAAQIRAxIUIVFSkdEEMUFhYpKhBRMzcYGxwSIjMtIVY+Hw/9oADAMBAAIRAxEAPwD+qaIiAiIgIiICIiAiIgIuvX10Fso5quqkENPC0ve88dB94cSfkHEqvx2isyxoqbvJU0FA8Hk7RDJyZLT0Gd7fSLv6DXBo10O/pqvWmi8ZqptH/upbJurvduoH7lTX0tO/+bLM1p/tK6/lVZPjig70z6Vw0mE49Qs3KexW2Fumh3KSMa/f4cVz+S1l+KKDuzPoWf7Pf6Gp88qrJ8cUHemfSnlVZPjig70z6V98lrL8UUHdmfQnktZfiig7sz6E/Z7/AEXU+eVVk+OKDvTPpTyqsnxxQd6Z9K++S1l+KKDuzPoTyWsvxRQd2Z9Cfs9/oanzyqsnxxQd6Z9K+tyizPcA270DiegCpZ9KeS1l+KKDuzPoXx2KWR7S11noHNPSDSs0P9ifs9/oaknHIyVgexwew8Q5p1BX6VbkwK20zzPZw7H6vUHlLcBGx2nU+LTcePvt19RB0K7djvM9TPNbrlEynutMA54j15KeM9EsWvHdPQWni12oOo3XOxmimYzUTf7pbYmURF4oIiICIiAiIgIiICIiAiIgIiIKzkOl0ymw2d+66nAkuczHa+nyLoxGPySSMf8AfjCsyrNxb4ptBstS7Xk6qiqaMEN1HKb0crRr1atZKfyKzLYxP40RGz8ys9giz/8Azhdlf/3Lw/8AX1L7RHeEJssaSDtKxAEcCDfqXh//AEWujgs+3C3ZFn1yxi1Y/kNxZba19trL3BRsNvgqmRCV0LnmQPBALRrubu84De4hVXYpt+vef4vlV1veFXyk5orriyM0tNC8TsgnfG2njYyd731Aa3Rw0DS4HdcRooWqxLJ7vt2tGT4jipxq3zXBk12yekvkMtBf7byJDd+lY7V0xJZuPLPRA13yCAI2PAtqFo2e7T8Es9mkt89dcbldLRk9NdIY2VLKirE/i4AdysMpjfKzfLd1pAId1oNHs/hE2W4UWWOuFgyLG7njVrdequz3mkjiqpaQNkIlh3ZHMeCYnt92NHDQ6KnZ/wCFHcaXZVTZfi2EZA6lrK61xUlVc6WnZHUwVUzWufGw1Afru+g0uAG/LEeLCXClWrYfkNPec6q7Jswiwq1XzAquxU9Cy5001RJXauLHTlryNZOU3Q/ff/s9XluoWn7Q9mmR33wabJjdsoopMmtVPZ6ltvmnaxsstHLTyvh5TUtBPJOaHa7upHHTig1zHrtNfbLSV9Ra62yzTt3nUFx5Pl4eJGj+Te9mvDX0XHpUis6pdueMWukhiza8WTAMhc0vmsN6vtGKmBpcdwu3ZSCHNAcND1rl/wA4XZYP/qXh/wCvqX2iDQFWM40tzbXe2aNmoKuKN7uOpgme2KRv3vSa/T1xt9S7+MZhYc3t76/Hb3br/QskMLqq11cdTE2QAEsLmEgOAc06dPEetdDaG3xjH46FuplrqympmADXpmaXH8jGvP5FsdH+LTHZfX8u30WOtZ0RFroIiICIiAiIgIiICIiAiIgIiII3ILK2+2/kOUNPURSMnp6gN1MUrDvNdpqNRqNCNRq0uHQV17Lkba2bm+vYygvUbdZaNz9d8DpkiJ05SM9TgNRro4NdqBNLo3ex0F+phBcKWOpjad5u+PSY7+c1w4tPyggr2pqiYyV9X2Xuly820nwWD9GPoTm2jH/lYP0Y+hQHkIItW01/vtLHpoGCuMug+/KHn+sr55ET9qb9+ni9kssmHv8ApK2jatAaGgAAADgAOpfVVvIiftTfv08Xsk8iJ+1N+/TxeyT3eHv+klo2rSiyvZrbbrlViuFXX5TeBNBerpQM5CaEN5Knrp4ItfsZ9Lcjbr8uvAdCtfkRP2pv36eL2Se7w9/0ktG1Y5aKnnfvSQRSO/nOYCV+ObKP4JB+jH0Kv+RE/am/fp4vZL75DzdeUX5w6x4xEP7RHqnu8Pf9JLRtTlXV0Fionz1EsFDStOrnvIY3U8B+U8Aoi2U02QXaK91kD6amp2ubbqWdpbKN4aPmkafcucODWn0mtLt7QvLWctuwq10FWyse2e4VsfFlTcKh9Q9h001ZvkhnD+aB0n1lTyk1U0RMUa5nt5J1dQiIvBBERAREQEREBERAREQEREBERAREQEREBERBn2xEtOJ3jdJI8p790jr51qtes9f/AGHQtBWf7EdfJS8alp/lNfvcgAf71qvV1/2+vjqtAQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREGe7DgBiV40c1/8AKe/8WjQf72quHQOI6PpWhLPdh2nkleNCSPKi/wDSNP8Ai1UtCQEREBERAREQEREBERAREQEREBERARVa8ZVXG4T0FkooKyWmIbU1FXO6KKNxAIY3da4vdoQSOAAI468F0efMw+A2PvU3s1tU9GxKoidUfWFsu6Kkc+Zh8BsfepvZpz5mHwGx96m9mstFr2xxgsu6Kkc+Zh8BsfepvZpz5mHwGx96m9mmi17Y4wWXdFSOfMw+A2PvU3s058zD4DY+9TezTRa9scYLLuqRtq2i1OyXZdkGX0tkkyKW0wtqHW6KbknSM5RrXnf3XabrC5/QeDerpTnzMPgNj71N7NcFfXZRdKGoo6u12CopKiN0M0MlRMWyMcNHNI5PiCCQmi17Y4wWed/Ar8LGs205HdcWoMGfQUEVVcb1WXZ9yD2wCpq5ZmRhghbvO3pQ33QJDXO6iF7GXnDweNhld4OFhvFtsNNaat1zrn1c1VUVEok3OiKLhHxawEgesucevQaxz5mHwGx96m9mmi17Y4wWXdFSOfMw+A2PvU3s058zD4DY+9TezTRa9scYLLuipHPmYfAbH3qb2ac+Zh8BsfepvZpote2OMFl3RUjnzMPgNj71N7NOfMw+A2PvU3s00WvbHGCy7oqRz5mHwGx96m9mv0zJMppTytVaLbVQN4vjoqt4m3evcD2Brj8hc0H1hNFr2xxgsuqLrW24093t9PW0knK01QwSRv0I1B9YPEH5DxHWuytSYmJtKCIigIiICIiDP8YOtXkRPTzvPx/NU6oLF/trIvxvUf4VOrsYv8uH2WesREXkgi6Jvlvbe2Wc1sHOr6d1WKLlByphDg0ybvTu7zgNejUrvICIiAi6NkvlvyS1wXK1VsFxt84JhqqaQSRyAEglrhwI1B4hd5AREQEXRuN8t9nmoYa6tgpJq+cU1LHNIGunl3XO3GA+6dutcdB1NJ6l3kBEUPiWXWnOrBT3ux1fj1sqHSMin5N8e8WSOjf6LwCNHMcOI6uHBQTCLo3S+W+yGjFwrYKM1lQ2kphPIGmaZwJbGzX3TiGuOg46AnqXeVBERB19l51wmj+SapA+QCok0VrVU2Xe8qk/D1P7xIrWtXpPx6/nP3WeuRERayCIiAiIgz/F/trIvxvUf4VOqCxf7ayL8b1H+FTq7GL/AC4fZZ63lW/zbRNrG07aJR2WqqaSHG6yO3UMNNlcto8W1p2SCeSCOllE++57iDI7d0buho0JPBtjzDM9mldiFLV5lHQ3rK7TBa8jcySWals72vijku1O3TSEb0ro9SGNLnxE+4ctzzTYPgu0G+G8Xywtqbk+EU808FVPTGoiHQyYRPaJWj1PDhpwUpPstxaruF9ram0RVVRfKFlsr3VEj5Wy0rGua2FrXOIYz0nEhgaCTqdTxWtllGPS7OqA+FlZITd8gPieGNqI3891QfMYayJgEhEn2RrhoXtdq15OrgSV+NnszoqfatnWU5ZkktDjmQ3tkFLDcZeQpaSJp1AhB3ZCAXFoeCGlrN0DQ66ldNhuF3mDHYqu1TSOx+Hxe3VAuFSyeGL0fsbpWyB8jfRb6L3OB0U5bMBsFntt8t9NbYxQ3uqqKy408rnSsqJZ/wDbFweTwd1tGjfUAmXWPM2EZZmmz/OKOaZt+ls17xS5XimteRZAbtUSy04hkieRyYFO9wkLTGxzmne6i1aRsTwOfLMEx3MrvnWUXe6X+0iqrWRXeSKj3qiHVzIoWaNi5MvIa5m64FgJOqt2L+D/AIHhl5t12tNjdT3O3Neylq5a6pmkijcwsMQMkjtYt0nSM6sB0IAIBX7xnYJgeG5Iy+2Wwi318ckksTYqufxeF8gIe6OAv5KMkOcDusHSUimY6xSPAsxmmsuwPGq+GsuNRLcKbelirK+aohiLJZBpFG9xbEOJ1DANSBr0KyeFBe7ljuwnKrjaLhUWq5QRwGGspHlkkRNRENQfvEjToIJB4Fdym2QwYKK2p2cMtuN3GvqDLV85R1VbSFpLnOEVOKmNsRL3A6s0HTw46j7UYHk2Z0NXZc+uOO33GauMNnorZa6qhme4Oa5h5Xxx+gDmgkAcfWraYiwzV2zyrm283HCxneasscuLx3fcF+n5VlYamSHlGya7zW6DXk2kRk9LdAAKNZdo+f7WbZslx+GqmkmuWKSXqukgvkllluE8crIdPGIoZH+iNXljA3e39SdG6H1iMRtLcvflApP9euoRbTVco/jTiQyBm5ru+7cTrprx010VVrtgGA3HE7Djc1gHNVhBFrEdXPHPRg9PJ1DXiUa9fp8eGuugUyz2DCMt2eZTKdk1uz+8V3jQzSemo5bZf6h0zKJ9FM+MSVDGQl8zXMc0SbodukjX0nazm3mqutxvV8tWHV2Vc54lj8dVWVUOSut9FSEtlfC97dx7quZwjcXNeN0ho1cCSVs1dsUwu44RR4jUWRrrDRTiqpoG1EzZIZg5z+VbMHiQP3nvO+HaneOp4ldO6+D/AIDfKiinr7D40+lo47eBJWVBbPTx67kc7eU0qGjU6cqH9JTLIyvGL3etu+Z2G03XJbtjtBTYZbL++nsNYaKavqqsO35HSM9Ixx7mgYOG8/jrwCpez2eouWxrZlhlomySsyKqN4q2RWm+m0RmCKuka+apqGMc7QOewNaxp1LjqNF6GuXg9YBdqGwUlRYnblhp/E7dJDXVMU0EH/o8qyQPdHw9w5xb8i+SeDxs/kx+x2RthMFvshmNuFPXVEMtOJXF0rRKyQPLHE8WFxaeA00AUyyPPNSbjtN2SbEZ8ou10ddYc7ks89XR3OWGR7WSVkTXmSIs3pQIWAS6B3F5Gm+4G27c5bpV3a82fDa/LDcsPx2Opq6uPJ30NJSasldC+QFkj6udwjcXB/okNGrgXErYXbB8DOGT4m3HoYselrecBQwzSxtgqN4O5SEteHQkEagRloGp06TrwXHwetn93no5a2wGqfTUrKL7LW1DhPA0ktjqBymlQ0Fx05Xf6SmWbCf2YX+qyvZrid7rS11bcrRSVk5aNAZJIWPdoOoauKsyjMZxu3Ydj9vslogdS2ughbT00DpXycnG0aNaHPJcQBwGp4AAdSk16QOvsu95VJ+Hqf3iRWtVTZd7yqT8PU/vEita1uk/Hr+c/dZ65ERFrIIiICIiDP8AF/trIvxvUf4VOqOuVoulguVdVWy387UddL4xJTxzNjmhl3Wtdu75DXNdu69IIJPug70erztf+xtz71R+3XYm2J+qmY4xH3llMX1ptFCc7X/sbc+9Uft052v/AGNufeqP26nu/FHmp5lk2ihOdr/2NufeqP26c7X/ALG3PvVH7dPd+KPNTzLJtFXK7JL3QQiSTC7w/V7GBsMtLK4lzg0HdbMTpq4au6GjUuIAJHY52v8A2NufeqP26e78UeanmWTaKE52v/Y2596o/bpztf8Asbc+9Uft0934o81PMsm0UJztf+xtz71R+3Tna/8AY2596o/bp7vxR5qeZZNooTna/wDY2596o/brry5Leoa2Klfhl4a+VjpGv5WlMejXNaQX8tutcS9ujSQXeloDuu0e78UeanmWWNFCc7X/ALG3PvVH7dOdr/2NufeqP26e78UeanmWTaKE52v/AGNufeqP26c7X/sbc+9Uft0934o81PMsm0UJztf+xtz71R+3X6ZV5JWnkocZmoZHcBPX1UBiZ/SIjkc46eoAa+sdKZPFHmjmlkjsu95VJ+Hqf3iRWtR2PWaPHrLSW6OR0zYGaGV/S9xOrnH75JP5VIrnY1UV4tVUdUzJPWIiLxQREQEREBERAREQF07ndI7VFC+SOaYzTxwMjgjL3Fz3Buug6GjUucegNa4ngF+LteqWzMpvGJWslq520tNFo4umldqQ0BoJPAOcSAd1rXOOjWkjrWWzyRSi53KOA3yaFsU76d8joo2gk7kYeeAGo1cA3fLQ4gcAAWixviqW3O58hVXoxyQ+MxMLWxQukLxEwEnTQbgc4aGQxtJA0a1swiICIiAiIgLqXa1Ul8t09DXQielmbuvYSQfWCCNC0ggEOBBBAIIIXbRBD0VdVUVx5vuJfUyVEk0tLUwUrxEIgQRHI4atY9odujUjfDdRx3gJhdK82elv9qqrdWtkdTVLDG/kpXxSN9TmSMIcxwOhD2kOaQCCCAV1LPd5ZK2otlxfRxXOIuljhgqA98tNvaMmLCAW69BGhAcCASNCgmEREBERAREQEREBERAREQEREBfCdASepfVXM4t5vltpbPLbIbrb7jVNp66Geo5Jrafdc950B1f7gN3B0hx19HeQc2OMq698l4rWV9DLVMDGWuqmY5lMxrnbrt1g0D3ghztS4jg0Hgp1EQEREBERAREQEREBQWVxPpqWO7wSx009tJmll8S8ZkfSgh08LGt9MF7WDTc47zGHR2m6Z1EHBQV0F0oaespZWz0tRG2aKVvQ9jhq0j74IXOoDFKx5fd7bNPWVU9urXxmesg5PfZIBNGGO6JGNbK2PeHXGQeIKn0BERAREQERQl4zbHsfqvFrnfLfQVOm9yNRUsY/T17pOuizpoqrm1MXlbXTaKredTDu1Fp75H9KedTDu1Fp75H9K9dGxtyeErlnYtKKredTDu1Fp75H9KedTDu1Fp75H9KaNjbk8JMs7FpRVbzqYd2otPfI/pTzqYd2otPfI/pTRsbcnhJlnYtKyvNNrmz6jy3HqetynEXVlruc3jPj1/p4J7Y8UtRGXiMvBL9X8kWkagSuP3KtfnUw7tRae+R/SvAXhleD/ZNpnhA4vfcWvVt5tyWVlPfKiCoYWUL2ab1Q/QgAOjH5XMPW4Jo2NuTwkyzsf0et9wpbtQU1dQ1MNbRVMTZoKmnkEkcsbgC17XDg5pBBBHAgrsKi45mmAYpj1rsltyO0wW620sVHTReOsO5FGwMY3p6mtAUj51MO7UWnvkf0po2NuTwkyzsWlFVvOph3ai098j+lPOph3ai098j+lNGxtyeEmWdi0oqt51MO7UWnvkf0p51MO7UWnvkf0po2NuTwkyzsWlFVvOph3ai098j+lfqPafiErw1uT2gknTjWxj5B1+tNHxtyeEplnYs6L8se2Roc0hzSNQQdQQv0tdBERBXY5DS7QZ4zPc5G1tsY9kLma0MJhlcHOa77mV/LsBaelsTSPclWJVy8yCnzPG3mW6jlmVVOIaVutG4ljH71R6iOSIYfW9w+6VjQEREBERB0r1WOt1nrqpgBfBBJK0H1taSP7lUcSpI6fH6KQDenqYWTzzO4vmkc0Fz3E8SST/8AHQFZ8q97F4+ZzfsFV7Gfe5avmkX7AXRwNWFPzZdiSREWTEREQEREBERAREQEREBERAXwgOBBGoPAgr6iDp4DJ4vU3+1x+jR0NWwU8Q6ImPhjeWN/ohxcQOgB2gAACt6puD++LLvncH7tGrktbpXxZ+UfaFnrERFqoruTPMd8xNwddwHXJ7C23N3oDrSVB/0v1QjQaH/1eRHWrEq5lp3bhi53rwP9agaWr/Zn/R5xpVf+x1/hBErGgIiICIiCLyr3sXj5nN+wVXsZ97lq+aRfsBWHKvexePmc37BVexn3uWr5pF+wF0cH4M/P8Mux3aqpioqaaomeI4YmGR7z9y0DUn+pYbjXhKXS/XzCDV4Y2zYvl/jMttvFVdWukMEVPJPvSQtj+xucxgcG75GhOrgQAd1la18T2vaHMIIc1w1BHXqF4Z2G1lvgzvG8cmbb80ic6sttPT2q63CR2PQzRvMsgpKimYII9AI/Tkc9ocAHO46yqbTDFrWP+GjYr7fbK0UlqbYrzXxUFHPDkVLNcmulfuRSTUDfTjYXFuvpOc0OBc0cdJin8JS6Oo3Xupwg02JQZC/Haq6c6sdLHIKw0rJmwcn6UZfub2rmuBcQGuADnd3ZJs52g7N4bHi9VLilzxGz6wQ3QxzC5zUzWkQsdHuiNr2+gC8PIIb7nU6ro1Owi/zbGbziLay2i5VuUuvkcplk5EQG7NrN0nc13+TaRpoRvcNdOKxjMI/OvDEs+JZFkNHSUlor6HHpn01wfV5JS0VbJIwAytpaST0pt3Xd4lm84FrddFarXtwuuWbRK/GsXxSO60NFS22vlvFTc/FohTVbS4Hc5Jzt8NaSGdDtHauZoAYyg2WZ5geVZOcSkxW4Y7kF1kvLjf2TiqoJ5tDO1gjaWysLgXNBcwguI1KumJ4FX2DaxnmTzS0pt19p7ZDSxROdysZp2TNfvgtAAPKN00J6DrorGbtFPf4Rsto2r2/Db9YbfbmXK4Ot1JNTZBT1dYH7rnRPmpGgPiZIGcHau0LmhwBK6Gyja7ljztQumbUdDTYxjt3uAdcIrhyslLHBHE7xdsIgZvsDC53KF28SdN3rVfsvg6ZvZoMXtrJcUdQ49kwv5uX2fx+76yyFxnduaRybkzuIMm85rBq0K3QbFshNTtJxurqLVUYFmlRWVklS2SVtypZKinbG9jWbhjc0OYHBxcDx4hT9QjMC8Li2Zhl2P2epobTSw5BI6KgfbskpbjVRP5N0jW1VPF6UJc1pGoLwHaNJGq36UvbE8xtD5ACWtJ0BPUNepZHs9tGf4LQ00WXjF6uw2Sgcx1ws1NVS3GsEbAGP5AM0a4taS5rOULidG6dCn6Xbji9ZVQ08cGSCSV4Y0yYpdWN1J0GrnUwDR8pIA61lE265GEY3mGdV+B7aM8vlFXvqbbJdKCGiostlhhpoYHuZM2nYKfcjlhZEXMn3HOkcTqGarQcKye81e1ytp6Ooq6+Buz6119JbLhcH8k+pfNUjee/dID3brGulDNSBrodAFJ0Wxy9U2yDalirqqgNxymrv1RRSiR/JRtrXSmESHc1BHKDe3Q7TjpvLtY1stv8AjG0+x5HBV22a2+S9Nj91p3mQTNfTmR8csBA0cC6Qgh27wGo48FjETqFF2Y+ERfLD4OdvzjaFSUj31HJQ0NVFco2vuU8sz2ASB0cUdMAd3Ulzmhoc46buh56Tww6J9kzGSa1WqrvGP2V99ZTWPIoblS1UDXBjmmojZrG8Oc0FrmdDgRqvxb/B6zMbKaXBqm52KBmM18Nyxm7wiaSR8sNQ6WMVcLmhoaWu3HBjndJPyKy5Ps/2g7Q9lGc45fYcRtdyu9tNFbxaJKh0TXuDt900j4w7dJ3NGtYdNDxdrwRmGhYFkV+ye2Or71j0eOxTBklHB48KiZ8Tm66ytDGiN410LQ54/pKzrhooXU1HBE4gujjawkdGoGi5l6DoYP74su+dwfu0auSpuD++LLvncH7tGrkvDpXxfpH2hZERFqIrmXPDK3GtZbrHvXVoAtg1Y/7DL6NR/wCz1n+kI1Y1XMul5Osxocvc4d+6tbpbmbzJPsMp3Z/VFw1J/nBisaAiIgIiIIvKvexePmc37BVexn3uWr5pF+wFabzRuuNorqRhAfPBJECeouaR/wDKp+JVkc9ho4AdyppYWQVFO7g+GRrQHNcDxBB+TiNCOBC6GBrwpjvZdiZREWbEREQEREBERAREQEREBERARF8e9sbS57g1o4kuOgCDo4P74su+dwfu0auSqGAR+M1F+usfpUdfVsdTS9UrGQxs32/0S4OAPQQAQSCFb1rdK+LPyj7Qs9YiItVFcy2YRV2MtM9zg5S6taBb27zJPsEx3aj1RcNdf5wZ61Y1XcqmMd1xVgqLjByt0Ld2gj3o5dKWodu1B+5i9HXX+eIx1qxICIiAiIgKGvGF4/kNQJ7pY7bcpwN0S1dJHK4D1auBOimUWVNdVE3pm0nUq3mswzslZP1fF9VPNZhnZKyfq+L6qtKL20jG354yyzTtVbzWYZ2Ssn6vi+qnmswzslZP1fF9VWlE0jG354yZp2qt5rMM7JWT9XxfVTzWYZ2Ssn6vi+qrSiaRjb88ZM07VW81mGdkrJ+r4vqqj59s6xakzXZrDBj1pp4Kq9VEVRFHRxNbUMFsrXhjxoN4BzWu048WA6cNRsKz/ae91Pk+zGpDt1kWSuZJxPESW2ujA4f0ns6eHD16JpGNvzxkzTtS/mswzslZP1fF9VPNZhnZKyfq+L6qtKJpGNvzxkzTtVbzWYZ2Ssn6vi+qnmswzslZP1fF9VWlE0jG354yZp2qt5rMM7JWT9XxfVTzWYZ2Ssn6vi+qrSiaRjb88ZM07VW81mGdkrJ+r4vqr9w7MsPp5A+PFbLG8cQ5tviB/ZVmRNIxt+eMmadr4AGgAAADgAF9RFrsRERBXMjmAyTE4fGrhTudWTSclSM1hnApZhuVB6mDeDh63sYrGq7cp2yZ5Y6UVVxie2iq6kwQM/0SVodAz7M7+eDJqxvX6Z+5ViQEREBERAREQEREBERAREQFn+3KN9PgZvMcRmfYK+jvLmgEnkYKhj59AOOvICbT5T19B0BcdRTxVcEkE8bJoZWlj45GhzXtI0IIPSCOpB+2uDgCCCDxBHWvqoOy+aXGGy4FXuldU2OFvN9TM4u8dtxcWwP3j7p8YaIpNdXbzWvOglZrfkBERAREQEREBERARF1rlWOt9vqaptNNWOhjdIKemaDJKQNQ1oJA3j0DUgceJCCIts3j2Z3iRstzayiggo3QTM3KNzyDKZIuGr37r2NcegaADjvKwKKxm2yWy1Bs81ZNUTyyVUorqjl3xPkeXmIOHDcZvbjQ0aBrR98yqAiIgIiICIiAiIgIiICIiAiIggcuxOLKKWBzJ32+7UTzNb7lC3WSll3SN4Doc0g6OYeDgSD1EdbE8ululTPZ7xTstuS0bA+eka4mOePXQTwOPu4ifytJ3XAHTWzqDyrE6bKaenL5ZKK4UcnL0NxpiBNSy6abzSeBBHBzDq1zSQ4EFBOIvDGz3/KFz554SuN4eyKhZh9QJbVLcII3jx+scdIqqMP0dFGXMDWxuJIErt4uIbu+50BERAREQERedvDa8JCu8HPZ1aa6xmmfkVyuUcdNDVM32OhjIkn1HqLd2MkaECXUEEAgPQdbWQ26jnqqiQRU8EbpZJD0Na0ak/kAUJTW45LVUtzudLC6kgMdXbKaaF7Z6eQsIc+UOOm/o/QN3dWaO9Il3o5z4Ou3Gw+EzjEOVUckVNW26oc19kZO50tvc6MNBm6A8u0lLHgBu68j3TXabOgIiICIiAiIgIiICIiAiIgL45wa0kkADiSepfVwV32lUfg3f3KxrkUamr71ltJFc4bzUWOiqmCSlpqOnhMgiOhY6R00b/TI4kANDd7d4lu879czXztree70P8MvzgXvFxz8W03/ACmqdXZrmKKppppi0d0cmUzaUJzNfO2t57vQ/wAMnM187a3nu9D/AAym0WGfwx5Y5F2AXvwI9nF/vsF5nhq6S5QSNmjntjKah3XtILXbsELBqCB1LZOZr521vPd6H+GU2iZ/DHljkXQnM187a3nu9D/DJzNfO2t57vQ/wym0TP4Y8sci6E5mvnbW893of4ZOZr521vPd6H+GU2iZ/DHljkXQnM187a3nu9D/AAyzjal4L+Nbarlba7Nbreb7PbmOjpRJJBHHG1x1d6EcTWkkgcSCeAWxImfwx5Y5JdScP2Xx7PrM2041fKyx21rt/wAWoaC3xMLtAC46U3F2jWjU6ngPUpvma+dtbz3eh/hlNomfwx5Y5LdCczXztree70P8MvrbTfYzqMyuzyOgS01EW/l0pwf7QppEz+GPLHJLubFL5PdoaynrWxtuFBMIJ3QgiOTVjXtkaDqQHNcOGp0IcNTpqZ1UzCPfNl3zin/d2K5rQ6RTFGJMR3TxiJJERFroIiICIiAiIgLgrvtKo/Bu/uXOuCu+0qj8G7+5WOuBRsC94uOfi2m/5TVOqCwL3i45+Lab/lNU1NDHUQyRSsEkUjS1zHDUOB4EFdfG+JV85Wet+0XlPZpZLpcc4tOzCrp5Zrbsqq6m4CWfXcqw4EWcb3XuxSyk+p1O1UvYzhNXn1qxjK6jOsWs+cTXVslbUyUlQ2+GqjmJmo5HOrQ0gta5nJclu7h4NHArWzdyPcChsbzC1ZbJeGWuoNQbTXyWyrJjczcqGNa57RqBrpvt4jh6l552O2fBMuuV5yXPKmin2jUmU1VO83OvMc9A5lUWUlPC0vG7GWCLdaBo/eOu9qobCbFZcKx7wgrjiltttBtCobleYbcaeJja1kPi0UsTY2+6Ld4b7QBoSEzD1wi8kYpbcQxXLtjFTs8rI6i63+lqDfH0tYZn3Ck8Sc989X6R1e2cRkOdod4lo9SvfgaYBYbHsQwzIKa3xm/V9oY2pucmr53xl28I988QxujQGjgN0cFYqvNhviLPPCAxlmY7Jb5Z35BTYyKrkGivrZTHTkiZhEUrg5p3JSBGQCCRJoNeg+YbzkNLdcZxTELfbrJguNx5dV2fImOmlrbHLVspBJA3fjlhc6CVzm6MLmAPaA4HQ6pqsPcKLyNXYzR7Odnd+tclxxnMrNkl/t1opbLb3z0Vns9U7i90p8ZlexhAje6IOAJ0Gn2RUqupDadkG3vD6e622e22y62XxePH3SNpKR00lOZWwNfLI5mj2nUbxAeHaAdAmce7lXLzndBY83xvFp4al9wv0NXNTSRtaYmNp2xmTfJcCCeVbpoD0HXTrwzahguH4vtA2cYdW0tPY9nV3luFVcKfljDT3K5MihEDaqTUF5LQ9wD3em5g13iF18r2b7P73tT2P4zaqSirsRZBkO9Q0dW6SmMgFKXRu3XnVod0xk6DQDThok1SPTyLHvBjj5uxnLrHFJIbbY8rudtt8UsheYKZkgcyIFxJ3W75A1PAaDqWwrOJvAj8I982XfOKf93YrmqZhHvmy75xT/u7Fc14dK+L9I+0LIiItRBERAREQEREBcFd9pVH4N39y51w1jS6jnABJMbgAOvgrHWKLgXvFxz8W03/ACmqdUFgWnkLjmhBHNtNxB1B+xN61Orr43xKvnKz1uGOip4aqapjgiZUzBrZZmsAfIG67ocek6anTXo1KhmbP8XjyR2Qtxu0Nv7vdXUUEQqjw04y7u90fKp9F4ogK7Z9i10yCK/VmNWirvkWnJ3OegifUs06NJS3eGnVoVzjDrAMkOQix20X8x8kbr4pH41uaabvK6b2mnDTVTCJYQVjwLGcYrqyts+O2m01lbr4zUUNDFDJPx1O+5rQXcePHVde6YW7ydorPjV1mwimpHDkhZKSl3WxgEckI5YXsa3Ug+i0HgOOmoNlRLCiUOzOsmdLBkuXXHNbPNGY5bRe7dbjTScQQXCOmYToRwGunyKfiwTGoMbdjsePWqPH3Ah1qbRRClIJ1IMW7u9PHoU4iWFfh2e4tTY3Lj0ONWeKwS8ZLUygiFK/iDxiDd08QOrqC/EezfEoaKWjjxayspJYo4JIG2+ERvjjfvxsLd3Qta4lzR0A8RxVjRLQI6/45acqtsluvdrorxb5CC+kr6dk8TiOjVjwQf6l1rZhGO2U282+wWugNubI2iNNRxx+KiTTlBHutG4Hbrd7d010GvQppEHTttmt9mFV4hQ01D41O+qn8WhbHy0zvdyP0A3nnQauPE6LuIioj8I982XfOKf93YrmqbhA/lJlp6R4zTjUevxdnD+0f1q5LX6V8X6R9oWRERaiCIiAiIgIiICIiCmy4fdrYXRWO5UcFBqTHS19K+XkdfuWPbI30OnRpB010BAAA4+YMw+NLH3Cb2yuyLbjpWJ22n6Qt1J5gzD40sfcJvbJzBmHxpY+4Te2V2RXSsTZHCFupPMGYfGlj7hN7ZOYMw+NLH3Cb2yuyJpWJsjhBdSeYMw+NLH3Cb2ycwZh8aWPuE3tldkTSsTZHCC7KcJrcvzK01la2qstGKe519t5N1FM4u8Wqpaff15UcHclvadW9px6VYOYMw+NLH3Cb2y6mxCQSYneCG7o8p78NOHVdaodQHq+nXpWgppWJsjhBdSeYMw+NLH3Cb2ycwZh8aWPuE3tldkTSsTZHCC6k8wZh8aWPuE3tk5gzD40sfcJvbK7ImlYmyOEF1J5gzD40sfcJvbL63H8uc4B12srG9bm26Ykfk5dXVE0rE2Rwjkl0ZYLHFYaJ0TZZKmeV5mqKmX3c0hABcdOA4AAAcAAAOhSaItWqqapzVdaCIixBERAREQEREBERAREQEREBERAREQZ9sRcXYpeCZOV/lPfhvEk6f61quHH1dH5FoKz7YhIZcTvBIA0ye/N4EnoutUOs/J/2WgoCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIq9n+fWHZfiNwyfJq026x0AYamqbBJNyYe9sbTuRtc4+k9o4A6a6ngCUFf2HlpxO8boaB5T3/3GumvOtVr09fr6tejgtBXnTwX/AAj9n20KS5YvYchlut9lu95ubYG2+raBSyXCeWJ7pHxBjQY5I9ASCNQ3TXgvRaAiIgIiICIiAiIgIiICIiAiIgIiIOpdLpS2WhlrKyXkaePTV2hcSSdA1rQCXOJIAaASSQACSq27aG/X0MYvsjepwjgbr+R0oP8AWF+c5eXX7EojxjNbNIW9W82ml0P5NT/0Au8uhh4dEURVVF787fhlqh0vOJL2Vv35lP7ZPOJL2Vv35lP7Zd1F6ZcLc9Z5l42Ol5xJeyt+/Mp/bJ5xJeyt+/Mp/bLuomXC3PWeZeNjpecSXsrfvzKf2yecSXsrfvzKf2y7qJlwtz1nmXjY6XnEl7K378yn9sojLr1R5xi91x+74dfam2XOmkpaiMspuLHtIOn2bgRrqD1EAqyImXC3PWeZeNjy/wCBnsQn8Gm2ZHNdceuVyv8AdKosbVUrYC1lGw/Ym6ukBDnElzhxGu7xOi9J+cSXsrfvzKf2y7qJlwtz1nmXjY6XnEl7K378yn9snnEl7K378yn9su6iZcLc9Z5l42Ol5xJeyt+/Mp/bJ5xJeyt+/Mp/bLuomXC3PWeZeNjpecSXsrfvzKf2yecSXsrfvzKf2y7qJlwtz1nmXjY6XnEl7K378yn9spOxZfS3updSOpqu21wbvilrotxz2jgXMIJa/ThruuJGo1A1GvCoTI3mK4Y3K3hI26xNDh1BzHsd/W1xH5U91h4n6YptPzNUr8iIuWxEREBERAREQUzOPfJiPzqo/d5FIKPzj3yYj86qP3eRSC6kfCo+X5lZ7BFn22XaDc8EtuO09kpqOe9ZDeYLLRyXEuFNA+RsjzJIGkOcA2JwDQQS4tGoWTVXhK5VitqySjv1Laa3J6bKY8ZoDaqKrfSEupW1Dp3sYZZn6N3iY2DeB0brx3hhNUQj00i8yz+EpmlqwvLayosVLXV9odbX0VyfaLja6GsbUVkcEkJjqmiRkjA7XeaXt9Np0OhabBftveQbKK7MqPOqW1XJ9ox+PIaOWwxywNma6Z0Hi72yPed7lAwB44EO13Rpopmgb0i87YZt9zOuyOG33a0Q19LV0VVUCroceu1vjt0sURkayZ9XG1srHbpaHNLDvaejx4d/FNq+e3HYbDtDv9dhePxV1rpquljqoqlsEDnubq+WQSEuDmnVkTWh285rd93SmaBvSLyJl+3fLc12EbYqVlXR26/4zSQyc7W6iraFs9NOwuBjinLZopBuPG8S5vQRqCtH2ibYMo2WWbEbLXPtVzzDIJagR1lHZ66Wjp4IWNe+Q00Lpp5HAPjboHAEu1JaAmaBuiLMdh+0y+bQqS+Q361Oo6q2VLIorhHbauhpq+N7A4PjiqmNkaWnea5p3gCAQ4ghT21nLKnCMFr7vSXGzWqohMYbVX7lDSt3nhpBbH6b3EE7rG8XO0HDVZX1XFwReZqPwnskOzHO7k+3WusyTF7nbqNrhS1dFSVsVVLA1ruSnAmiduyvHHeALQ4bzToZ+/bY8/wetzSy3Ky2rJcgtuOsyG1x2KGeJszTK+J8MjHve5xYWh2rSN4a6NBWOaBvSLBLZtvyKDFMUyWpuuJZJYbhkMFquFdYIp2Mp4J2cnG4iSQmKRlQ6Nr2v19Fw4NPRHxeFNVX+0ZDDZLdTR385FS2fHo6xj3Q19NPMYo6wgOaSz7BWP4EejEPWmaB6LReW7p4VuTVVZeLpjtjZdLFbq+ajhtUdhutRWXFkMpjkfHVxRGmjJLXlrTvdADnNOoGi4zn+cZltczKxUUdioMZxqvooXz1VNNJV1Mc1LFM+MASNax433emQRxaNw6ElmiRr6gsn+28d/G8H+JTqgsn+28d/G8H+JbGF/Lj9ljraAiIuOgiIgIiICIiCmZx75MR+dVH7vIpBR+ce+TEfnVR+7yKQXUj4VHy/MrPYyLwosdflGzAUTKe4VTRcKeZ7LbZ+dJAGEuBMIkjfoCAd6N4eOGmoJBzvZnstvW0DAauz11HJhjbDeoLtjOQQWR1sqpKkMJkmmo5pZHOGrnMPKO+yNcejTVeoUXnNN5ujKb/ALIcmzXZ1d8byfOI7pV11XR1MdbBZ2U8VM2CeKbcbEJCTvGLQlzzpvajgNFz55sItm0fKr3cbxWPfbbtjRx2agij3XtHLmYTtl14OBI0G70tB1PQtPRW0DN8S2eZpbKWrosj2heU1A63voYIuZ46Z4LgAJpXh7jI8AEcNwHeJI100j6zYP4xsXxPB4766CvxkW+Whu/igc01FIWmOR8Bdo5pLeLN7r6eGq1hEtAxVvg6Vd2j2htyXLpL07N7XDQV7obeymFO+JsjY5IAHu3Whsg9B28SW6l3HRdm67Eckv1qxyrr88BzfHKqSa15FS2hkbGxSRNjkhmpjI4SNeAS4hzTrpppothRMsDPoLnlez+008F1pLxtLuNTLJJJWWWjoqKOmaA0Nj5OWoZoOkg7zz7rUjgoXK7Pctt1rpKOSyX3AbjZbjTXi3XG8QUdTCaiJx3QYoal5eNHO1BLOkEHULW0Swwqu8Gy6XmgzeO6Zsa+tyue11dVUm1NjbDLRzMeOTY2Qeg5kbGBpJI03i52uiueSbL7jc8/uGXWnJDZLlPj4skBFC2fkHCoMwm9J2jund3CPl1WhImWBitN4Nza3Dto1syDIBdLvm5a+suFLb20kNPKyJrIZI4A93pNc1ryS8lzhxIU7H4P+M0uZYDkFLEaaTDbdJbaKBrQWyRmMRxl59cYMunDplcVpqJlgZFYtimQ4Vf63yWzuS0YnW3R11msctqiqHxvkk5SaOGdzhuRvdveiWOLd47pB4q3Yhs+8lMyze/eP+NeU1bT1ni/I7ni3JU0cG7vbx39eT3tdBprpodNVb0S0QCgsn+28d/G8H+JTqgsn+28d/G8H+Je+F/Lj9ljraAiIuOgiIgIiICIiCsZta6mo5quVJC6qltlQ6Z9Oz3csbonscGetw3g4Dr3SOtQb88s0bi18tTG8dLX0M7XD74LNQtDRbmHj000xTXTe3fb8St9rOvL+x/CZ+5zfUTy/sfwmfuc31FoqL00jC3J4x/U1M68v7H8Jn7nN9RPL+x/CZ+5zfUWiomkYW5PGP6mpnXl/Y/hM/c5vqJ5f2P4TP3Ob6i0VE0jC3J4x/U1M68v7H8Jn7nN9RPL+x/CZ+5zfUWiomkYW5PGP6mpmtNtKx2sY59PXvnY17oy6OlmcA5ri1zeDOkOBBHUQQuXy/sfwmfuc31F+9hzWtxK8BpJHlPfzx06edqrVaEmkYW5PGP6mpnXl/Y/hM/c5vqJ5f2P4TP3Ob6i0VE0jC3J4x/U1M68v7H8Jn7nN9RPL+x/CZ+5zfUWiomkYW5PGP6mpnXl/Y/hM/c5vqJ5f2P4TP3Ob6i0VE0jC3J4x/U1M68v7H8Jn7nN9RclK/yzutqNFDUNt9DUirnq54HwtcWtcGxsDwC4lxBJHABp46kBaCik9JoiP0UzE983/ELeOwREWgxEREBERAREQEREBERAREQEREBERBn+xHTyUvGgA/lNfugg/wDFar1f9/XxWgLPtiJa7E7xukkeU9+GpOvHnWq16h/16+laCgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIM92HaeSV40008qL/wC51+Nqr1rQlnuw8AYneNBp/Ke//dB3/FarrH93V0LQkBERAREQEREBERAREQEREBERAREQEREBERARFTNoO0WHDo2UlLGysvUzN+Onc4hkTCSOVkI+51BAA4uIIGgDnN9sHBrx64w8OLzIuaLzHdb3eMgldJc7xWVBd/4MUzoIW8eqNhA4dGp1Py8Soo2unJ1Ik1/DP+lfSU+wqpj9eJae6L/mDU9YqibdMTvucbJMnsuM3itsOQ1FLv0Ffb6h9PMyZjmyMaJGkFoeWbh0PQ49Swnmqn9Un6Z/0pzVT+qT9M/6Vl/gf9vp/wBLwzX/ACbOP7QclumR5dleVZJVWW3zzUNNbK+6VD4Jq17i6okfG55a5zS466ji+QnpC99ryVBYaGlYWwwmFrnF5bHI5oLidSeB6SSSSuTmqn9Un6Z/0p/gf9vp/wBLw9YovJ3NVP6pP0z/AKVz00T6FwdSVlbRyDofTVcsZH9TlJ9g7MX0/wCl4eqkWLYbtcrbTOylyOoFZbjwFxc0Nlp/VygaNHM6t7QFvSd4akbQCHAEEEHiCFweldExeiV5cSPlPZI+oiLSBERAREQEREBERAREQEREBERBxzzx0sEk0rgyKNpe9x6gBqSvLhutRkFRPd6vXxm4P8Yc0nXcB9wwfI1u638nX0r0vkFDJc7DcqOI6S1FNLEw66cXMIH968vWiQS2qjeOgws4aaacBw06l9b7CpptiV9uqPpr/wDfQnqdtERfVsHQvt+t+MWqouV1q46KhgAMk0p4DU6AeskkgADiSQAq3Btkw6ez3C588tgpLe6JtWamCWGSDlXhkZfG9oeGuc4DeI06TroDpEbesauORYtaZbfTVldzXd6a41NHb53Q1M8DN4PETmlpDxvBw0IOrOHFUPJcUoL7hOQXCx2DL3XZ77dS79/NXLPNE2tilc2OOZ7n7rNHOJ0AGpI1Gq5+NjYtFcxREWiL6769U9Stix/aNjuTuuDaC4+nQMEtSyqhkpnRxkEiQiVrSWENPpD0eHSqpQbb7Xk20HGrHjtVFX0NfDWS1U0lNNG4CNrDG6Jzg1rmkl+rgHA6cCFX9r2E3vLMryuntVLODW4b4pFUbpbFLMKpzuR3+jeLdRpr0O9S7NvvVTmG0zZ9VU2K3yyUdso6+Op5wtz4Iqdz44g2MOI0PuCARwPUT1YVY2LmyTq1x2Tr19mzV19Y2ZERdNBbBsPvklfjdVa53l8lpn5CIuOpMDmh0ev/AOOrmD5GBY+tJ2CUrzUZPW9MT5aemHq3mMc539krVxva9NNXRKpnri1uNvtLOnta6iIvgAREQEREBERAREQEREBERAREQFge0rDJMQu89whYeY62blBIDwppnnjG71Nc46tPRq4t4ehvb4uOeCOphkhmjbLFI0sfG9oc1zSNCCD0grf6F0yroeJnp1xPXA8hZFhGPZc+B98slBd3QAiI1tOyUsB0103gdNdB/UojzMYFpp5G2PT8XxfVXo+7bC7PUyuktlbWWYH/AMCFzZIRx14NeCW/eBAHq6NIo7BKjXhlEncWfWX1ke0ugYn6qtU98cokt3sbx3BcdxGWaWyWO32mSZobI+ipmRF4HEA7oGqnFpHmEqO1EncWfWTzCVHaiTuLPrL2p9p9Bpi1NdvpPIy97N117jbqW70M9FXU8VXSTsMcsEzA5j2npBB4ELUPMJUdqJO4s+snmEqO1EncWfWV/wAr0KdU1+k8jL3sFbsZwJrg5uG2MEHUEUEXD/8AVfun2PYNSzxzw4hZIpo3B7JGUEYc1wOoIOnAgrd/MJUdqJO4s+suem2CRl2lXkdbJH1imgiiJ/KQ7T/roXjPtD2fGuJjyzyMvezSjpKq618Nvt8PjVwn15KHe04Dpc4/ctGo1d8oA1JAPofDcXhw/Hqa2RPMz2bz5pyNDLK4lz3fJxPAdQAHUvuM4facQpnw2ukEJkIMsz3F8sp9bnu1J6ToOga8AFNL572j7RnplqKItRHGTq6hERcQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREH//2Q==",
            "text/plain": [
              "<IPython.core.display.Image object>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "try:\n",
        "    display(Image(app.get_graph().draw_mermaid_png()))\n",
        "except Exception:\n",
        "    # This requires some extra dependencies and is optional\n",
        "    pass"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "3-edfZVbzaso"
      },
      "source": [
        "## Step 10: Execute the graph"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "5MUXBSpPvekk"
      },
      "outputs": [],
      "source": [
        "def execute_graph(thread_id: str, question: str) -> None:\n",
        "    \"\"\"\n",
        "    Execute the graph and stream its output\n",
        "\n",
        "    Args:\n",
        "        thread_id (str): Conversation thread ID\n",
        "        question (str): User question\n",
        "    \"\"\"\n",
        "    # Add question to the question and memory attributes of the graph state\n",
        "    inputs = {\"question\": question, \"memory\": [HumanMessage(content=question)]}\n",
        "    config = {\"configurable\": {\"thread_id\": thread_id}}\n",
        "    # Stream outputs as they come\n",
        "    for output in app.stream(inputs, config):\n",
        "        for key, value in output.items():\n",
        "            print(f\"Node {key}:\")\n",
        "            print(value)\n",
        "    print(\"---FINAL ANSWER---\")\n",
        "    print(value[\"memory\"][-1].content)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 41,
      "metadata": {
        "id": "nG3VBRyTzaso",
        "outputId": "77bb5c56-f118-4bea-d5b9-7dcb9b038b25"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "---EXTRACTING METADATA---\n",
            "---CHECK FOR METADATA---\n",
            "---DECISION: GENERATE FILTER---\n",
            "Node extract_metadata:\n",
            "{'metadata': {'metadata.custom_metadata.company': ['WALMART INC.'], 'metadata.custom_metadata.year': ['2023']}}\n",
            "---GENERATING FILTER DEFINITION---\n",
            "Node generate_filter:\n",
            "{'filter': {'$and': [{'metadata.custom_metadata.company': {'$eq': 'WALMART INC.'}}, {'metadata.custom_metadata.year': {'$eq': 2023}}]}}\n",
            "---PERFORMING VECTOR SEARCH---\n",
            "Node vector_search:\n",
            "{'context': 'DOCUMENTS INCORPORATED BY REFERENCE\\n\\nDocument Portions of the registrant\\'s Proxy Statement for the Annual Meeting of Shareholders to be held May 31, 2023 (the \"Proxy Statement\")\\n\\nParts Into Which Incorporated Part III\\n\\nWalmart Inc. Form 10-K For the Fiscal Year Ended January 31, 2023\\n\\nTable of Contents\\n\\nWALMART INC. ANNUAL REPORT ON FORM 10-K FOR THE FISCAL YEAR ENDED JANUARY 31, 2023\\n\\nAll references in this Annual Report on Form 10-K, the information incorporated into this Annual Report on Form 10-K by reference to information in the Proxy Statement of Walmart Inc. for its Annual Shareholders\\' Meeting to be held on May 31, 2023 and in the exhibits to this Annual Report on Form 10-K to \"Walmart Inc.,\" \"Walmart,\" \"the Company,\" \"our Company,\" \"we,\" \"us\" and \"our\" are to the Delaware corporation named \"Walmart Inc.\" and, except where expressly noted otherwise or the context otherwise requires, that corporation\\'s consolidated subsidiaries.\\n\\nPART I\\n\\nWalmart International includes numerous formats divided into two major categories: retail and wholesale. These categories consist of many formats, including: supercenters, supermarkets, hypermarkets, warehouse clubs (including Sam\\'s Clubs) and cash & carry, as well as eCommerce through walmart.com.mx, walmart.ca, flipkart.com, walmart.cn and other sites. Walmart International had net sales of $101.0 billion for fiscal 2023, representing 17% of our fiscal 2023 consolidated net sales, and had net sales of $101.0 billion and $121.4 billion for fiscal 2022 and 2021, respectively. The gross profit rate is lower than that of Walmart U.S. primarily because of its format mix.\\n\\nWalmart International\\'s strategy is to create strong local businesses powered by Walmart which means being locally relevant and customer-focused in each of the markets it operates. We are being deliberate about where and how we choose to operate and continue to re-shape the portfolio to best enable long-term, sustainable and profitable growth. As such, we have taken certain strategic actions to strengthen our Walmart International portfolio for the long-term, which include the following highlights over the last three years:\\n\\nDivested of Walmart Argentina in November 2020.\\n\\nDivested of Asda Group Limited (\"Asda\"), our retail operations in the U.K., in February 2021.\\n\\nDivested of a majority stake in Seiyu, our retail operations in Japan, in March 2021.\\n\\nOmni-channel. Walmart U.S. provides an omni-channel experience to customers, integrating retail stores and eCommerce, through services such as pickup and delivery, in-home delivery, ship-from-store, and digital pharmacy fulfillment options. As of January 31, 2023, we had more than 4,600 pickup locations and more than 3,900 same-day delivery locations. Our Walmart+ membership offering provides enhanced omni-channel shopping benefits including unlimited free shipping on eligible items with no order minimum, unlimited delivery from store, fuel discounts, access to Paramount+ streaming service, and mobile scan & go for a streamlined in-store shopping experience. We have several eCommerce websites, the largest of which is walmart.com. We define eCommerce sales as sales initiated by customers digitally and fulfilled by a number of methods including our dedicated eCommerce fulfillment centers and leveraging our stores, as well as certain other business offerings that are part of our flywheel strategy, such as our Walmart Connect advertising business. The following table provides the approximate size of our retail stores as of January 31, 2023:\\n\\nOur strategy is to make every day easier for busy families, operate with discipline, sharpen our culture and become more digital, and make trust a competitive advantage. Making life easier for busy families includes our commitment to price leadership, which has been and will remain a cornerstone of our business, as well as increasing convenience to save our customers time. By leading on price, we earn the trust of our customers every day by providing a broad assortment of quality merchandise and services at everyday low prices (\"EDLP\"). EDLP is our pricing philosophy under which we price items at a low price every day so our customers trust that our prices will not change under frequent promotional activity. Everyday low cost (\"EDLC\") is our commitment to control expenses so our cost savings can be passed along to our customers.\\n\\nOur operations comprise three reportable segments: Walmart U.S., Walmart International and Sam\\'s Club. Our fiscal year ends on January 31 for our United States (\"U.S.\") and Canadian operations. We consolidate all other operations generally using a one-month lag and on a calendar year basis. Our discussion is as of and for the fiscal years ended January 31, 2023 (\"fiscal 2023\"), January 31, 2022 (\"fiscal 2022\") and January 31, 2021 (\"fiscal 2021\"). During fiscal 2023, we generated total revenues of $611.3 billion, which was comprised primarily of net sales of $605.9 billion.'}\n",
            "---GENERATING THE ANSWER---\n",
            "Node generate_answer:\n",
            "{'memory': [HumanMessage(content='DOCUMENTS INCORPORATED BY REFERENCE\\n\\nDocument Portions of the registrant\\'s Proxy Statement for the Annual Meeting of Shareholders to be held May 31, 2023 (the \"Proxy Statement\")\\n\\nParts Into Which Incorporated Part III\\n\\nWalmart Inc. Form 10-K For the Fiscal Year Ended January 31, 2023\\n\\nTable of Contents\\n\\nWALMART INC. ANNUAL REPORT ON FORM 10-K FOR THE FISCAL YEAR ENDED JANUARY 31, 2023\\n\\nAll references in this Annual Report on Form 10-K, the information incorporated into this Annual Report on Form 10-K by reference to information in the Proxy Statement of Walmart Inc. for its Annual Shareholders\\' Meeting to be held on May 31, 2023 and in the exhibits to this Annual Report on Form 10-K to \"Walmart Inc.,\" \"Walmart,\" \"the Company,\" \"our Company,\" \"we,\" \"us\" and \"our\" are to the Delaware corporation named \"Walmart Inc.\" and, except where expressly noted otherwise or the context otherwise requires, that corporation\\'s consolidated subsidiaries.\\n\\nPART I\\n\\nWalmart International includes numerous formats divided into two major categories: retail and wholesale. These categories consist of many formats, including: supercenters, supermarkets, hypermarkets, warehouse clubs (including Sam\\'s Clubs) and cash & carry, as well as eCommerce through walmart.com.mx, walmart.ca, flipkart.com, walmart.cn and other sites. Walmart International had net sales of $101.0 billion for fiscal 2023, representing 17% of our fiscal 2023 consolidated net sales, and had net sales of $101.0 billion and $121.4 billion for fiscal 2022 and 2021, respectively. The gross profit rate is lower than that of Walmart U.S. primarily because of its format mix.\\n\\nWalmart International\\'s strategy is to create strong local businesses powered by Walmart which means being locally relevant and customer-focused in each of the markets it operates. We are being deliberate about where and how we choose to operate and continue to re-shape the portfolio to best enable long-term, sustainable and profitable growth. As such, we have taken certain strategic actions to strengthen our Walmart International portfolio for the long-term, which include the following highlights over the last three years:\\n\\nDivested of Walmart Argentina in November 2020.\\n\\nDivested of Asda Group Limited (\"Asda\"), our retail operations in the U.K., in February 2021.\\n\\nDivested of a majority stake in Seiyu, our retail operations in Japan, in March 2021.\\n\\nOmni-channel. Walmart U.S. provides an omni-channel experience to customers, integrating retail stores and eCommerce, through services such as pickup and delivery, in-home delivery, ship-from-store, and digital pharmacy fulfillment options. As of January 31, 2023, we had more than 4,600 pickup locations and more than 3,900 same-day delivery locations. Our Walmart+ membership offering provides enhanced omni-channel shopping benefits including unlimited free shipping on eligible items with no order minimum, unlimited delivery from store, fuel discounts, access to Paramount+ streaming service, and mobile scan & go for a streamlined in-store shopping experience. We have several eCommerce websites, the largest of which is walmart.com. We define eCommerce sales as sales initiated by customers digitally and fulfilled by a number of methods including our dedicated eCommerce fulfillment centers and leveraging our stores, as well as certain other business offerings that are part of our flywheel strategy, such as our Walmart Connect advertising business. The following table provides the approximate size of our retail stores as of January 31, 2023:\\n\\nOur strategy is to make every day easier for busy families, operate with discipline, sharpen our culture and become more digital, and make trust a competitive advantage. Making life easier for busy families includes our commitment to price leadership, which has been and will remain a cornerstone of our business, as well as increasing convenience to save our customers time. By leading on price, we earn the trust of our customers every day by providing a broad assortment of quality merchandise and services at everyday low prices (\"EDLP\"). EDLP is our pricing philosophy under which we price items at a low price every day so our customers trust that our prices will not change under frequent promotional activity. Everyday low cost (\"EDLC\") is our commitment to control expenses so our cost savings can be passed along to our customers.\\n\\nOur operations comprise three reportable segments: Walmart U.S., Walmart International and Sam\\'s Club. Our fiscal year ends on January 31 for our United States (\"U.S.\") and Canadian operations. We consolidate all other operations generally using a one-month lag and on a calendar year basis. Our discussion is as of and for the fiscal years ended January 31, 2023 (\"fiscal 2023\"), January 31, 2022 (\"fiscal 2022\") and January 31, 2021 (\"fiscal 2021\"). During fiscal 2023, we generated total revenues of $611.3 billion, which was comprised primarily of net sales of $605.9 billion.'), AIMessage(content='During fiscal 2023, Walmart generated total revenues of $611.3 billion, primarily comprised of net sales of $605.9 billion. Walmart International had net sales of $101.0 billion, representing 17% of the fiscal 2023 consolidated net sales.')]}\n",
            "---FINAL ANSWER---\n",
            "During fiscal 2023, Walmart generated total revenues of $611.3 billion, primarily comprised of net sales of $605.9 billion. Walmart International had net sales of $101.0 billion, representing 17% of the fiscal 2023 consolidated net sales.\n"
          ]
        }
      ],
      "source": [
        "execute_graph(\"1\", \"Sales summary for Walmart for 2023.\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 42,
      "metadata": {
        "id": "bNpHPPdozaso",
        "outputId": "9f5af9a4-61b5-485b-ce77-2d9793dfbbda",
        "scrolled": true
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "---EXTRACTING METADATA---\n",
            "---CHECK FOR METADATA---\n",
            "---DECISION: SKIP TO VECTOR SEARCH---\n",
            "Node extract_metadata:\n",
            "{'metadata': {}}\n",
            "---PERFORMING VECTOR SEARCH---\n",
            "Node vector_search:\n",
            "{'context': ''}\n",
            "---GENERATING THE ANSWER---\n",
            "Node generate_answer:\n",
            "{'memory': [HumanMessage(content=''), AIMessage(content='You asked for the sales summary for Walmart for 2023.')]}\n",
            "---FINAL ANSWER---\n",
            "You asked for the sales summary for Walmart for 2023.\n"
          ]
        }
      ],
      "source": [
        "execute_graph(\"1\", \"What did I just ask you?\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 43,
      "metadata": {
        "id": "h8zPMULbzaso",
        "outputId": "a6b6484b-fbf3-43d9-b6bc-dc51abc7038d"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "---EXTRACTING METADATA---\n",
            "---CHECK FOR METADATA---\n",
            "---DECISION: SKIP TO VECTOR SEARCH---\n",
            "Node extract_metadata:\n",
            "{'metadata': {}}\n",
            "---PERFORMING VECTOR SEARCH---\n",
            "Node vector_search:\n",
            "{'context': ''}\n",
            "---GENERATING THE ANSWER---\n",
            "Node generate_answer:\n",
            "{'memory': [HumanMessage(content=''), AIMessage(content=\"I DON'T KNOW\")]}\n",
            "---FINAL ANSWER---\n",
            "I DON'T KNOW\n"
          ]
        }
      ],
      "source": [
        "execute_graph(\"1\", \"What's my name?\")"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3 (ipykernel)",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.12.1"
    },
    "widgets": {
      "application/vnd.jupyter.widget-state+json": {
        "state": {}
      }
    }
  },
  "nbformat": 4,
  "nbformat_minor": 4
}
